package org.afk.sisy.io;

import java.io.*;
import java.nio.file.*;
import java.sql.*;
import java.util.*;
import java.util.concurrent.*;

import org.afk.sisy.*;
import org.afk.sisy.db.*;
import org.afk.tools.io.*;
import org.afk.tools.log.*;

public class CompareJob implements Callable<CopyJob>
{
	private final PathWrapper primary;
	private PathWrapper secondary;
	private final SisyProgress progress;
	private final ErrorHandler errorHandler;
	private final SisyJob job;
	private final PathHasher pathHasher;
	private final PathDataBase pathDataBase;

	public CompareJob(final PathWrapper primary, final PathWrapper secondary, final SisyProgress progress, final ErrorHandler errorHandler, final SisyJob job,
			final PathHasher fileHasher, final PathDataBase pathDataBase)
	{
		super();
		this.primary = primary;
		this.secondary = secondary;
		this.progress = progress;
		this.errorHandler = errorHandler;
		this.job = job;
		this.pathHasher = fileHasher;
		this.pathDataBase = pathDataBase;
	}

	private CopyJob backupDeletedPrimary()
	{
		return new CopyJob(null, this.secondary, this.errorHandler, this.progress, this.job, "No primary matched", this.pathDataBase);
	}

	private void calculateHashForWrapper(final PathWrapper source)
	{
		if (source.getHash() == null)
			try
			{
				final StopWatch stopWatch = new StopWatch();
				final String hash = this.pathHasher.hash(source.getAbsolutPath());
				source.setHash(hash);
				this.errorHandler.debug("After " + stopWatch + " hashed " + source);
			}
			catch (final IOException e)
			{
				this.errorHandler.warning("While hashing " + source.getRelativePath(), e);
			}
	}

	private void checkForMatchingSecondary()
	{
		final Path target = this.job.getSecondaryDir().resolve(this.primary.getRelativePath());
		if (Files.exists(target, LinkOption.NOFOLLOW_LINKS))
			try
			{
				this.secondary = new PathWrapper(this.job.getSecondaryDir(), target);
				if (Files.isDirectory(target))
					this.progress.incTargetFolders(1);
				else
					this.progress.incTargetFiles(1);
			}
			catch (final IOException e)
			{
				this.errorHandler.warning("while creatung " + target, e);
			}

	}

	private CopyJob compare()
	{
		if (isSizeDifferent())
		{
			this.progress.incChanged(1);
			return createCopyJob("Size different");
		}

		final boolean lastModifiedChanged = isLastModifiedChanged();

		if (this.job.getCompareOption() == CompareOption.ATTRIBUTES)
			if (lastModifiedChanged)
			{
				this.progress.incChanged(1);
				return createCopyJob("Attributes changed");
			}
			else
				return nothingToDo("last modified unchanged");

		if (this.job.getCompareOption() == CompareOption.DB)
			return compareDataBaseHash();

		return compareFileSystemHash();
	}

	private CopyJob compareDataBaseHash()
	{
		try
		{
			if ((this.pathHasher != null) && !this.primary.isDirectory())
				calculateHashForWrapper(this.primary);
			if (this.pathDataBase.isChanged(this.primary))
			{
				this.progress.incChanged(1);

				return new CopyJob(this.primary, new PathWrapper(null, this.primary.getRelativePath(), this.job.getSecondaryDir().resolve(
						this.primary.getRelativePath()), this.job.getSecondaryDir(), 0, 0, this.primary.isDirectory()), this.errorHandler, this.progress, this.job,
						"differs from database", this.pathDataBase);
			}
			else
				return nothingToDo("Unchanged in DB");
		}
		catch (final SQLException e)
		{
			this.errorHandler.warning("while comparing in DB " + this.primary, e);
			return nothingToDo("Error " + e.getMessage());
		}
	}

	private CopyJob compareFileSystemHash()
	{
		if (isHashEqual())
			return fixLastModified("lastmodified changed , hash unchanged");

		this.progress.incChanged(1);
		return createCopyJob("hash and last modified changed");
	}

	private CopyJob createCopyJob(final String reason)
	{
		return new CopyJob(this.primary, this.secondary, this.errorHandler, this.progress, this.job, reason, this.pathDataBase);
	}

	private CopyJob fixLastModified(final String string)
	{
		this.errorHandler.debug(string + " " + this.primary + " -> \r\n" + this.secondary);
		this.progress.incSkipped(1);
		if (this.primary != null)
			try
			{
				Files.setLastModifiedTime(this.secondary.getAbsolutPath(), this.primary.getLastModified());
			}
			catch (final IOException e)
			{
				this.errorHandler.warning("Could not change last modified of " + this.secondary.getAbsolutPath());
			}
		updateDataBase();
		return null;
	}

	private boolean isHashEqual()
	{
		calculateHashForWrapper(this.primary);
		calculateHashForWrapper(this.secondary);
		if (Objects.equals(this.primary.getHash(), this.secondary.getHash()))
			return true;
		return false;
	}

	private boolean isLastModifiedChanged()
	{
		return !SisyTools.equalLastModifies(this.primary.getLastModified(), this.secondary.getLastModified());
	}

	private boolean isSizeDifferent()
	{
		return this.primary.getSize() != this.secondary.getSize();
	}

	private CopyJob matched()
	{
		if (this.primary.isDirectory())
			return nothingToDo("Both directories exist");

		final MatchOption matchOption = this.job.getMatchOption();

		if (matchOption == MatchOption.IGNORE)
			return nothingToDo("Ignoring matched file " + this.primary);

		if (matchOption == MatchOption.SYNC)
			return createCopyJob("Always Sync");

		if (matchOption == MatchOption.COMPARE)
			return compare();

		return nothingToDo("unknown option " + matchOption);
	}

	private CopyJob nothingToDo(final String string)
	{
		this.errorHandler.debug(string + " " + this.primary + " -> \r\n" + this.secondary);
		this.progress.incSkipped(1);
		updateDataBase();
		return null;
	}

	private CopyJob unmatchedPrimary()
	{
		progress.incNewPrimaryCount(1);
		if (this.job.getUnmatchedPrimaryOption() == UnmatchedOption.COPY)
			return createCopyJob("New Primary");

		if (this.job.getUnmatchedPrimaryOption() == UnmatchedOption.DELETE)
			return backupDeletedPrimary();

		return nothingToDo("ignore unmatched primary");

	}

	private CopyJob unmatchedSecondary()
	{
		progress.incNewSecondaryCount(1);
		if (this.job.getUnmatchedSecondaryOption() == UnmatchedOption.COPY)
			return createCopyJob("New Secondary");

		if (this.job.getUnmatchedSecondaryOption() == UnmatchedOption.DELETE)
			return backupDeletedPrimary();

		return nothingToDo("ignore unmatched in secondary");

	}

	private void updateDataBase()
	{
		if (this.pathDataBase != null)
			try
			{
				this.pathDataBase.update(this.primary);
			}
			catch (final SQLException e)
			{
				this.errorHandler.warning("while updating in DB " + this.primary, e);
			}

	}

	@Override
	public CopyJob call()
	{
		if (!this.job.isScanSecondaryDir())
			checkForMatchingSecondary();

		if (this.primary == null)
			return unmatchedSecondary();

		if (this.secondary == null)
			return unmatchedPrimary();

		return matched();
	}
}
